<?php

require_once 'SessionDelegate.php';
require_once 'HTTP/Client.php';
require_once 'HTTP/Request/Listener.php';
require_once MOBILEPROXY_SPYC_DIR . '/spyc.php';
require_once MOBILEPROXY_MPC_DIR . '/MobilePictogramConverter.php';

class MobileProxy
{
    var $session = null;
    var $proxyUrl = '';
    var $clientType = '';
    var $requestParams = array();
    var $url = '';
    var $response = array();
    var $baseUrl = '';
    var $encoding = '';
    var $output = '';

    /**
     * @return string
     * @access public
     * @static
     */
    function execute()
    {
        $proxy =& new MobileProxy;
        $proxy->main();
    }

    /**
     * @return void
     * @access private
     */
    function MobileProxy()
    {
        $this->session =& SessionDelegate::getInstance();
        $this->requestParams = array(
            'url'       => '',
            'has_utn'   => false,
            'has_guid'  => false,
            'posts'     => array()
        );
        $this->response = array(
            'code'      => 0,
            'headers'   => array(),
            'body'      => ''
        );
        $schema = (isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] == 'on')? 'https': 'http';
        $this->proxyUrl = "$schema://$_SERVER[HTTP_HOST]$_SERVER[SCRIPT_NAME]";
    }

    /**
     * @return void
     * @access private
     */
    function main()
    {
        $this->session->set('response_url', '');
        $this->session->set('response_headers', array());
        $this->session->set('response_body', '');
        $this->processRequestParams();
        if (!$this->sendRequest()) {
            header('Content-Type:text/plain;charset=UTF-8');
            echo $this->output;
            exit;
            break;
        }
        $this->session->set('response_url', $this->url);
        $this->session->set('response_headers', $this->getResponseHeaders());
        $ctype = '';
        if (isset($this->response['headers']['content-type'])) {
            $ctype = $this->response['headers']['content-type'];
        }
        if (!$ctype) {
            $ctype = 'application/octet-stream';
        }
        $array = explode(';', $ctype);
        switch (trim($array[0])) {
            case '':
            case 'text/html':
            case 'text/xhtml':
            case 'application/xhtml+xml':
            case 'application/vnd.wap.xhtml+xml':
                $converter =& new MobileProxy_HTMLConverter(
                    $this->response['body'],
                    $this->url,
                    $this->proxyUrl,
                    $ctype,
                    $this->clientType
                );
                $this->output = $converter->convert();
                @mb_convert_variables('UTF-8', $converter->getEncoding(), $this->response);
                $this->session->set('response_body', $this->response['body']);
                if ($this->response['code'] == 200) {
                    $this->updateHistory();
                }
                header('Content-Type:text/html;charset=UTF-8');
                echo $this->output;
                exit;
                break;
            case 'text/vnd.wap.wml':
                header('Content-Type:text/plain;charset=UTF-8');
                echo 'Unable to handle content-type text/vnd.wap.wml';
                exit;
                break;
            default:
                header("Content-Type: $ctype");
                echo $this->response['body'];
                exit;
        }
    }

    /**
     * @return void
     * @access private
     */
    function processRequestParams()
    {
        /**
         * Post
         */
        if (isset($_POST['__post_params'])) {
            $params =& $_POST['__post_params'];
            if (isset($params['action'])) {
                $this->requestParams['url'] = $params['action'];
                $method = (isset($params['method']) and strtolower($params['method']) == 'post')? 'post': 'get';
                $encoding = 'SJIS-win';
                if (isset($params['encoding']) and @mb_preferred_mime_name($params['encoding'])) {
                    $encoding = $params['encoding'];
                }
                if (isset($params['has_utn']) and $params['has_utn']) {
                    $this->requestParams['has_utn'] = true;
                }
                /**
                 * Posted from the basic authentication form handled by JavaScript.
                 */
                if (isset($params['realm']) and isset($params['username']) and isset($params['password'])) {
                    $array = array(
                        'user' => $params['username'],
                        /**
                         * The base64 encode is of course unnecessary here.
                         * I don't just like to save the password as it is :-p
                         */
                        'pass' => base64_encode($params['password'])
                    );
                    if (!$auth = $this->session->get('auth')) {
                        $auth = array($params['realm'] => $array);
                    } else {
                        $auth[$params['realm']] = $array;
                    }
                    $this->session->set('auth', $auth);
                    /**
                     * Restore the post params which may be saved
                     * if the last challenge (refused by the server) was a post request
                     * and must be posted again.
                     */
                    if ($last_post = $this->session->get('last_post')) {
                        $this->requestParams['posts'] = $last_post;
                    }
                /**
                 * Posted from normal forms.
                 */
                } else {
                    /**
                     * Clean out the saved params anyway.
                     */
                    $this->session->set('last_post', array());
                    unset($_POST['__post_params']);
                    @mb_convert_variables($encoding, 'UTF-8', $_POST);
                    switch ($method) {
                    case 'post':
                        $this->requestParams['posts'] = $_POST;
                        $this->session->set('last_post', $_POST);
                        break;
                    default:
                        $array = array();
                        foreach ($_POST as $key => $value) {
                            $array[] = urlencode($key) . '=' . urlencode($value);
                        }
                        $this->requestParams['url'] = "{$this->requestParams['url']}?" . implode('&', $array);
                    }
                }
            }
        /**
         * Get
         */
        } else {
            if (isset($_GET['url'])) {
                $this->requestParams['url'] = $_GET['url'];
            }
            if (isset($_GET['has_utn'])) {
                $this->requestParams['has_utn'] = (boolean) $_GET['has_utn'];
            }
        }
        $this->requestParams['url'] = MobileProxy::normalizeUrl($this->requestParams['url']);
        $this->requestParams['has_guid'] = MobileProxy::hasGuid($this->requestParams['url']);
    }

    /**
     * @param  string   $url
     * @return string
     * @access private
     * @static
     */
    function normalizeUrl($url)
    {
        $url = str_replace('&amp;', '&', trim($url));
        if (false === strpos($url, 'http')) {
            $url = "http://{$url}";
        }
        $search = '/^https?:\/\/[0-9a-z][0-9a-z\-\.]+[0-9a-z]+(?:\:\d+){0,1}(\/?[\w\-\.\!\~\*\'\(\)\;\/\?\:\@\&\=\+\$\,\%\#]*)$/';
        if (!preg_match($search, $url, $matches)) {
            $url = '';
        } elseif (empty($matches[1])) {
            $url .= '/';
        }
        return $url;
    }

    /**
     * @param  string   $url
     * @return boolean
     * @access private
     * @static
     */
    function hasGuid($url)
    {
        @list($path, $query) = explode('?', $url);
        if (!isset($query) or empty($query)) {
            return false;
        }
        return (bool) false !== array_search('guid=ON', explode('&', $query));
    }

    /**
     * @param  array    $params
     * @return boolean
     * @access private
     */
    function sendRequest($params = array())
    {
        if (empty($this->requestParams['url'])) {
            return false;
        }
        if (!$headers = $this->session->get('request_headers')) {
            $array = Spyc::YAMLLoad(MOBILEPROXY_REQUESTHEADERS_YAML);
            $headers = $array[0]['headers'];
        }
        $headers = $this->getRequestHeaders($headers);
        /*
        if ($this->clientType == 'docomo'
            and isset($headers['x-dcmguid'])) {
            if (!$this->requestParams['has_guid']
                or 0 === strpos($this->requestParams['url'], 'https')) {
                unset($headers['x-dcmguid']);
            }
        }
        */
        $cookie_manager =& new HTTP_Client_CookieManager_Extended;
        if ($this->clientType != 'docomo') {
            if (!$cookies = $this->session->get('request_cookies')) {
                $cookies = array();
            } else {
                foreach ($cookies as $key => $value) {
                    $cookie_manager->addCookie($value);
                }
            }
        }
        $client =& new HTTP_Client_Extended($params, $headers);
        /**
         * I like to use the cookie manager as a reference. But HTTP_Client takes a copy
         * if it is attached by its standard way (the third param of the constructor).
         * So I dare to do this though I think this is really bogus.
         */
        $client->_cookieManager =& $cookie_manager;
        $client->attach(
            $request_listener =& new HTTP_Request_Listener_Extended
        );
        if (empty($this->requestParams['posts'])) {
            $result = $client->get(
                $this->requestParams['url']
            );
        } else {
            $result = $client->post(
                $this->requestParams['url'],
                $this->requestParams['posts']
            );
        }
        if (PEAR::isError($result)) {
            $error_message = 'Failed to connect to the requested URL: ' . $result->getMessage();
            $this->output = $error_message;
            return false;
        }
        $this->response = $client->currentResponse();
        $this->url = $request_listener->getFinalUrl();
        /**
         * If the server requests basic authentication:
         * Send the same request again with the corresponding username/password if saved.
         * If not saved or the username/password is not authorized,
         * echo JavaScript code which will tell the parent frame to show the authentication dialog.
         * Note that the sleep function will save a time for the inevitable delay of
         * (asynchronous) show/hide action of the dialog form controlled by JavaScript.
         */
        if ($this->response['code'] == 401 and isset($this->response['headers']['www-authenticate'])) {
            static $retry = false;
            $urls = parse_url($this->requestParams['url']);
            $key = md5("$urls[host]{$this->response['headers']['www-authenticate']}");
            if (!$retry and $auth = $this->session->get('auth') and isset($auth[$key])) {
                $retry = true;
                return $this->sendRequest(array(
                    'user' => $auth[$key]['user'],
                    'pass' => base64_decode($auth[$key]['pass'])
                ));
            } else {
                sleep(1);
                echo "<script type=\"text/javascript\">if(parent.MobileSimulator&&parent.MobileSimulator.showAuthDialog)parent.MobileSimulator.showAuthDialog('{$this->requestParams['url']}','$key')</script>\n";
            }
        }
        if ($this->clientType != 'docomo') {
            $this->session->set(
                'request_cookies',
                array_merge($cookies, $cookie_manager->getCookies())
            );
        }
        return true;
    }

    /**
     * @param  array    $headers
     * @return array
     * @access private
     */
    function getRequestHeaders($headers)
    {
        $array = array();
        foreach ($headers as $header) {
            $name = strtolower(trim($header['name']));
            $value = trim($header['value']);
            if (!empty($name) and !empty($value)) {
                if ($name == 'user-agent') {
                    $value = $this->getUserAgent($value);
                }
                $array[$name] = $value;
            }
        }
        return $array;
    }

    /**
     * @param  string   $user_agent
     * @return void
     * @access private
     */
    function getUserAgent($user_agent)
    {
        /**
         * If the current request is made from the element which doesn't have utn attribute,
         * and if the User-Agent request header has DoCoMo's device ID, remove it.
         */
        if (!$this->requestParams['has_utn']) {
            $search = '/(\s*(?:\/\s*ser\w{11}|;\s*ser\w{15}\s*;\s*icc\w{20})\s*)/';
            $user_agent = preg_replace($search, '', $user_agent);
        }
        /**
         * Very rough client detection :-p
         */
        if (0 === strpos($user_agent, 'DoCoMo/')) {
            $this->clientType = 'docomo';
        } elseif (preg_match('/^(?:SoftBank\/|Vodafone\/|J-PHONE\/|MOT-)/', $user_agent)) {
            $this->clientType = 'jphone';
        } elseif (preg_match('/\bUP\.Browser\b/', $user_agent)) {
            $this->clientType = 'au';
        }
        return $user_agent;
    }

    /**
     * Format response header data for Ajax response
     * and return it.
     *
     * @return array
     * @access private
     */
    function getResponseHeaders()
    {
        $array = array();
        if (!empty($this->response['headers']) and $this->response['code']) {
            $array[] = array(
                'name'  => 'HTTP Status Code',
                'value' => $this->response['code']
            );
            foreach ($this->response['headers'] as $key => $value) {
                $keys = explode('-', trim($key));
                array_walk($keys, create_function('&$item', '
                    $item = ucfirst($item);
                '));
                $key = implode('-', $keys);
                $array[] = array(
                    'name'  => $key,
                    'value' => trim($value)
                );
            }
        }
        return $array;
    }

    /**
     * Prepare history data to be responded by Ajax
     * and save it in PHP session.
     *
     * @return void
     * @access private
     */
    function updateHistory()
    {
        $key = md5($this->url);
        $urls = parse_url($this->url);
        $array = explode('.', $urls['host']);
        if (count($array) > 2 and $array[0] == 'www') {
            array_shift($array);
        }
        $current = array(
            'url' => $this->url,
            'host' => implode('.', $array),
            'title' => '',
            'date' => time()
        );
        if (preg_match('/<title\b[^>]*?>(.+)<\/title>/si', $this->response['body'], $matches)) {
            $current['title'] = $matches[1];
        }
        if (!$histories = $this->session->get('histories')) {
            $histories = array($key => $current);
        } else {
            $histories[$key] = $current;
            MobileProxy::sortMultiArray($histories, 'date');
            $histories = array_slice($histories, 0, MOBILEPROXY_MAX_HISTORIES);
        }
        $this->session->set('histories', $histories);
    }

    /**
     * @param  array    $array
     * @param  string   $sortby
     * @param  integer  $sorttype
     * @param  integer  $sortorder
     * @return void
     * @access private
     * @static
     */
    function sortMultiArray(&$array, $sortby, $sorttype = SORT_NUMERIC, $sortorder = SORT_DESC)
    {
        $keys = array();
        foreach ($array as $key => $value) {
            $keys[$key] = $value[$sortby];
        }
        array_multisort($keys, $sorttype, $sortorder, $array);
    }
}

class MobileProxy_HTMLConverter
{
    var $html = '';
    var $srcUrl = '';
    var $baseUrl = '';
    var $encoding = '';
    var $response = array();
    var $proxyUrl = '';
    var $clientType = '';

    /**
     * @param  string   $html
     * @param  string   $src_url
     * @param  string   $proxy_url
     * @param  string   $content_type
     * @param  string   $client_type
     * @return void
     * @access public
     */
    function MobileProxy_HTMLConverter($html, $src_url, $proxy_url, $content_type = '', $client_type = '')
    {
        $this->html       = $html;
        $this->srcUrl     = $src_url;
        $this->proxyUrl   = $proxy_url;
        $this->encoding   = $content_type;
        $this->clientType = $client_type;
        $this->baseUrl    = $this->srcUrl;
        $this->response   = array();
    }

    /**
     * @return string
     * @access public
     */
    function convert()
    {
        $this->preProcess();
        $this->convertEncoding();
        $this->getBaseUrl();
        $this->rewriteLinks();
        $this->rewriteForms();
        if ($this->clientType != 'au') {
            $this->convertPcdataEmoji();
            $this->convertAttribsEmoji();
            $this->convertInputsEmoji();
        }
        $this->convertEmoji($this->html);
        $this->postProcess();
        return $this->html;
    }

    /**
     * @return string
     * @access public
     */
    function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * @return void
     * @access private
     */
    function preProcess() {
        $this->html = str_replace("\x0", '', $this->html);
        $this->html = preg_replace('/^\xef\xbb\xbf/', '', $this->html);
        /**
         * Convert all the HTML's minimizable attributes (including utn) to name="value"
         * format for getAttrib() and getAllAttribs() methods of this class to work properly.
         * This is bogus if DOCTYPE is HTML. You may rewrite it again according to the DOCTYPE,
         * for example in postProcess() method of this class - I dare to ignore it :-p
         */
        $html_minimized_attribs = array(
            'checked', 'compact', 'controls', 'declare', 'defer', 'disabled',
            'ismap', 'mayscript', 'multiple', 'nohref', 'noshade', 'nowrap',
            'readonly', 'selected', 'utn', 'wrap'
        );
        foreach ($html_minimized_attribs as $attrib) {
            $search[] = "/\b{$attrib}\s*=\s*[\'\"]?{$attrib}[\'\"]?/i";
            $replace[] = $attrib;
        }
        $this->html = preg_replace($search, $replace, $this->html);
        foreach ($html_minimized_attribs as $attrib) {
            $this->html = str_replace($attrib, "$attrib=\"$attrib\"", $this->html);
        }
    }

    /**
     * @return void
     * @access private
     */
    function postProcess()
    {
        /**
         * Add BASE href and LINK to the customizable CSS to the content.
         */
        $schema = (isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] == 'on')? 'https': 'http';
        $path = pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_DIRNAME);
        $css_url = "$schema://$_SERVER[HTTP_HOST]$path/moblook.css";
        $tag = "<base href=\"{$this->baseUrl}\"><link rel=\"stylesheet\" type=\"text/css\" href=\"$css_url\">";
        if (preg_match('/<head\b[^>?]*?>/si', $this->html, $matches)) {
            $this->html = str_replace($matches[0], "$matches[0]$tag", $this->html);
        } elseif (preg_match('/<html\b[^>?]*?>/si', $this->html, $matches)) {
            $this->html = str_replace($matches[0], "$matches[0]<head>$tag</head>", $this->html);
        } else {
            $this->html = "<html><head>$tag</head>{$this->html}";
        }
    }

    /**
     * @return void
     * @access private
     */
    function convertEncoding()
    {
        $encoding = false;
        if (isset($this->encoding)) {
            $encoding = MobileProxy_HTMLConverter::getCharsetFromCType($this->encoding);
        }
        /**
         * Detect META anyway even if the Content-Type header is available
         * for replacing the MIME part of META to UTF-8.
         */
        if (preg_match_all('/<meta\b[^>]+?>/si', $this->html, $matches)) {
            foreach ($matches[0] as $match) {
                if ($http_equiv = MobileProxy_HTMLConverter::getAttrib('http-equiv', $match)
                    and strtolower($http_equiv) == 'content-type'
                    and $content = MobileProxy_HTMLConverter::getAttrib('content', $match)) {
                    if (!$encoding) {
                        $encoding = MobileProxy_HTMLConverter::getCharsetFromCType($content);
                    }
                    $this->html = str_replace(
                        $match,
                        '<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">',
                        $this->html
                    );
                    break;
                }
            }
        }
        if (!$encoding) {
            /**
             * Usually I use mb_detect_encoding as a last resort.
             * But automatic encoding detection may fail if the content contains
             * binary characters (i.e. Emoji).
             * So now I fix the encoding as CP932 (a.k.a. SJIS-win for PHP).
             */
            //$encoding = @mb_detect_encoding($this->html);
            $encoding = 'SJIS-win';
        }
        if ($encoding) {
            /**
             * Character encoding here must be of Microsoft's codepage
             * or Emoji and MS Gaiji (which is in common use in Keitai)
             * will be missing.
             */
            switch ($encoding) {
            case 'SJIS':
            case 'Shift_JIS':
                $encoding = 'SJIS-win';
                break;
            case 'EUC-JP':
                $encoding = 'eucJP-win';
                break;
            }
            $encoding = @mb_convert_variables('UTF-8', $encoding, $this->html);
            $this->encoding = $encoding;
        }
    }

    /**
     * @param  string   $string
     * @return string
     * @access private
     * @static
     */
    function getCharsetFromCType($string)
    {
        $result = '';
        $array = explode(';', $string);
        if (isset($array[1])) {
            $array = explode('=', $array[1]);
            if (isset($array[1])) {
                /**
                 * MIME value can be varied in fact (not only lower/upper case).
                 * For example, in case of Shift_JIS: 'x-sjis' of the legacy expression,
                 * or 'shift-jis' which is a commonly used misspelling.
                 * mb_preferred_mime_name() will normalize these variations.
                 */
                if (!$result = @mb_preferred_mime_name(trim($array[1]))) {
                    $result = '';
                }
            }
        }
        return $result;
    }

    /**
     * @return void
     * @access private
     */
    function getBaseUrl()
    {
        if (preg_match_all('/<base\b[^>]+?>/si', $this->html, $matches)) {
            foreach ($matches[0] as $match) {
                if ($base_href = MobileProxy_HTMLConverter::getAttrib('href', $match)) {
                    $this->baseUrl = $this->getAbsoluteUrl($base_href, $this->srcUrl);
                    $this->html = str_replace($match, '', $this->html);
                    break;
                }
            }
        }
        if (empty($this->baseUrl)) {
            $this->baseUrl = $this->srcUrl;
        }
    }

    /**
     * @return void
     * @access private
     */
    function rewriteLinks()
    {
        $tags = array('a', 'area');
        foreach ($tags as $tag) {
            $this->html = preg_replace_callback(
                "/<($tag)\b[^>]+?>/si", array($this, 'rewriteLinksCallback'), $this->html
            );
        }
    }

    /**
     * @param  array    $matches
     * @return string
     * @access private
     */
    function rewriteLinksCallback($matches)
    {
        $tag_string = $matches[0];
        $tag = strtolower($matches[1]);
        $attribs = MobileProxy_HTMLConverter::getAllAttribs($tag_string);
        /**
         * The target attribute must be cleared.
         */
        if (isset($attribs['target'])) {
            unset($attribs['target']);
        }
        $has_utn = in_array('utn', array_keys($attribs));
        $array = array($tag);
        foreach ($attribs as $key => $value) {
            if ($key == 'href') {
                $params = $has_utn? array('has_utn' => true): array();
                $value  = $this->addProxyUrl($value, $params);
            }
            $array[] = "$key=\"$value\"";
        }
        return '<' . implode(' ', $array) . '>';
    }

    /**
     * @return void
     * @access private
     */
    function rewriteForms()
    {
        $this->html = preg_replace_callback(
            '/<form\b[^>]*?>/si', array($this, 'rewriteFormsCallback'), $this->html
        );
    }

    /**
     * @param  array    $matches
     * @return string
     * @access private
     */
    function rewriteFormsCallback($matches)
    {
        $tag_string = $matches[0];
        $has_utn = (bool) MobileProxy_HTMLConverter::getAttrib('utn', $tag_string);
        if (!$action = MobileProxy_HTMLConverter::getAttrib('action', $tag_string)) {
            $action = $this->baseUrl;
        }
        $action = $this->getAbsoluteUrl($action);
        if (!$method = MobileProxy_HTMLConverter::getAttrib('method', $tag_string)) {
            $method = 'get';
        }
        $params = array(
            'action'   => $action,
            'method'   => $method,
            'encoding' => $this->encoding
        );
        $result = '';
        $result .= "<form action=\"{$this->proxyUrl}\" method=\"post\">";
        $result .= "<input type=\"hidden\" name=\"__post_params[action]\" value=\"$params[action]\">";
        $result .= "<input type=\"hidden\" name=\"__post_params[method]\" value=\"$params[method]\">";
        $result .= "<input type=\"hidden\" name=\"__post_params[encoding]\" value=\"$params[encoding]\">";
        if ($has_utn) {
            $result .= "<input type=\"hidden\" name=\"__post_params[has_utn]\" value=\"1\">";
        }
        return $result;
    }

    /**
     * @param  string   $url
     * @param  array    $params
     * @return string
     * @access private
     */
    function addProxyUrl($url, $params = array())
    {
        $url = $this->getAbsoluteUrl($url);
        if (!$urls = @parse_url($url)
            or !isset($urls['scheme'])
            or !in_array($urls['scheme'], array('http', 'https'))) {
            return $url;
        }
        $fragment = isset($urls['fragment'])? "#$urls[fragment]": '';
        $url = str_replace($fragment, '', $url);
        $params['url'] = str_replace('&amp;', '&', $url);
        $array = array();
        foreach ($params as $key => $value) {
            $array[] = urlencode($key) . '=' . urlencode($value);
        }
        $querystring = implode('&amp;', $array);
        return "{$this->proxyUrl}?$querystring$fragment";
    }

    /**
     * @param  string   $url
     * @param  string   $base_url
     * @return string
     * @access private
     */
    function getAbsoluteUrl($url, $base_url = '')
    {
        $url = preg_replace('/\s/s', '', $url);
        if ($urls = @parse_url($url)
            and isset($urls['scheme'])) {
            return $url;
        }
        if (empty($base_url)) {
            $base_url = $this->baseUrl;
        }
        $bases = @parse_url($base_url);
        if (!isset($bases['path'])) {
            $bases['path'] = '/';
        }
        if (0 === strpos($url, '/')) {
            $path = $url;
        } else {
            if (preg_match('/^[\?#]/', $url)) {
                $base_path = $bases['path'];
            } else {
                $base_path = substr($bases['path'], 0, strrpos($bases['path'], '/') +1);
            }
            $path = Net_URL::resolvePath("$base_path$url");
        }
        return  "$bases[scheme]://$bases[host]"
            . (isset($bases['port'])? ":$bases[port]": '')
            . $path;
    }

    /**
     * Return the value of the specified attribute name
     * found in the given string (single open tag line).
     * Returns empty string if not found.
     *
     * @param  string   $name
     * @param  string   $string
     * @return string
     * @access private
     */
    function getAttrib($name, $string)
    {
        $result = '';
        $search = "/[\s\'\"]\b$name\b\s*=\s*([^\s\'\">]+|\'[^\']+\'|\"[^\"]+\")/si";
        if (preg_match($search, $string, $matches)) {
            $result = preg_replace('/^\s*[\'\"](.+)[\'\"]\s*$/s', '$1', $matches[1]);
        }
        return $result;
    }

    /**
     * Return a hash contains all the attributes (in name => value format)
     * found in the given string (single open tag line).
     * Returns empty array if not found.
     *
     * @param  string   $string
     * @return array
     * @access private
     * @static
     */
    function getAllAttribs($string)
    {
        $array = array();
        $search = '/([\w\-]+)\b\s*=\s*([^\s\'\">]+|\'[^\']+\'|\"[^\"]+\")/s';
        if (preg_match_all($search, $string, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $key = strtolower(trim($match[1]));
                $value = preg_replace('/^\s*[\'\"](.+)[\'\"]\s*$/s', '$1', $match[2]);
                $array[$key] = $value;
            }
        }
        return $array;
    }

    /**
     * @return void
     * @access private
     */
    function convertPcdataEmoji()
    {
        /**
         * Not only TITLE and TEXTAREA, but child of OPTION must be #PCDATA only too.
         * The problem is that the OPTION's close tag is often missing.
         * So it's not easy to get the child by a simple regexp.
         * You may add it - I dare to ignore it :-p
         * Ah, you may ignore SCRIPT and STYLE the child of which must be #PCDATA only too.
         */
        $tags = array('title', 'textarea');
        foreach ($tags as $tag) {
            $this->html = preg_replace_callback(
                "/<$tag\b[^>]*?>.*?<\/$tag>/si", array($this, 'convertPcdataEmojiCallback'), $this->html
            );
        }
    }

    /**
     * @param  array    $matches
     * @return string
     * @access private
     */
    function convertPcdataEmojiCallback($matches)
    {
        $this->convertEmoji($matches[0]);
        $matches[0] = preg_replace(
            MOBILEPROXY_MPC_TAGREGEXP,
            MOBILEPROXY_EMOJI_REPLACE,
            $matches[0]
        );
        return $matches[0];
    }

    /**
     * @return void
     * @access private
     */
    function convertAttribsEmoji()
    {
        $this->html = preg_replace_callback(
            "/<(?!input)(\w+)\b[^>]*?>/si", array($this, 'convertAttribsEmojiCallback'), $this->html
        );
    }

    /**
     * @param  array    $matches
     * @return string
     * @access private
     */
    function convertAttribsEmojiCallback($matches)
    {
        $attribs = MobileProxy_HTMLConverter::getAllAttribs($matches[0]);
        $this->convertEmoji($matches[0]);
        if (strtolower($matches[1]) == 'img'
            and isset($attribs['localsrc'])) {
            return $matches[0];
        }
        $matches[0] = preg_replace(
            MOBILEPROXY_MPC_TAGREGEXP,
            MOBILEPROXY_EMOJI_REPLACE,
            $matches[0]
        );
        return $matches[0];
    }

    /**
     * @return void
     * @access private
     */
    function convertInputsEmoji()
    {
        $this->html = preg_replace_callback(
            '/<input\b[^>]*?>/si', array($this, 'convertInputsEmojiCallback'), $this->html
        );
    }

    /**
     * @param  array    $matches
     * @return string
     * @access private
     */
    function convertInputsEmojiCallback($matches)
    {
        $tag_string = $matches[0];
        $attribs = MobileProxy_HTMLConverter::getAllAttribs($tag_string);
        if (!isset($attribs['value'])) {
            return $tag_string;
        }
        $this->convertEmoji($attribs['value']);
        if (isset($attribs['type'])
            and in_array(strtolower($attribs['type']), array('submit', 'button', 'reset'))) {
            $inner_html = $attribs['value'];
            foreach ($attribs as $key => $value) {
                $array[] = "$key=\"$value\"";
            }
            return '<button ' . implode(' ', $array) . ">$inner_html</button>";
        }
        $attribs['value'] = preg_replace(
            MOBILEPROXY_MPC_TAGREGEXP,
            MOBILEPROXY_EMOJI_REPLACE,
            $attribs['value']
        );
        foreach ($attribs as $key => $value) {
            $array[] = "$key=\"$value\"";
        }
        return '<input ' . implode(' ', $array) . '>';
    }

    /**
     * @param  string   $string
     * @return void
     * @access private
     */
    function convertEmoji(&$string)
    {
        switch ($this->clientType) {
        case 'docomo':
            MobileProxy_HTMLConverter::execMpc($string, MPC_FROM_FOMA, MPC_FROM_OPTION_WEB);
            break;
        case 'au':
            MobileProxy_HTMLConverter::execMpc($string, MPC_FROM_EZWEB, MPC_FROM_OPTION_WEB);
            break;
        case 'jphone':
            MobileProxy_HTMLConverter::execMpc($string, MPC_FROM_SOFTBANK, MPC_FROM_OPTION_RAW);
            break;
        }
    }

    /**
     * @param  string   $string
     * @param  string   $carrier
     * @param  string   $type
     * @return void
     * @access private
     * @static
     */
    function execMpc(&$string, $carrier, $type)
    {
        $mpc =& MobilePictogramConverter::factory(
            $string,
            $carrier,
            MPC_FROM_CHARSET_UTF8,
            $type
        );
        $mpc->setImagePath(MOBILEPROXY_MPC_IMAGEPATH);
        $string = $mpc->Convert(
            str_replace('MPC_', '', strtoupper(get_class($mpc))),
            MPC_TO_OPTION_IMG
        );
    }
}

/**
 * HTTP_Client doesn't offer a way to get the final (redirect) URL.
 * So I use the listener mechanism to have it.
 * Note that listeners attached to HTTP_Client are not passed to HTTP_Request.
 * It means that the events notified to the listeners are of HTTP_Client,
 * and not of HTTP_Request. Events available for HTTP_Request such as
 * 'gotHeaders' or 'gzTick' never been notified to this listener.
 * See the comment section of HTTP_Client::attach() for available events.
 */
class HTTP_Request_Listener_Extended extends HTTP_Request_Listener
{
    var $_url;
    var $_checkedUrl;
    var $_redirUrl;

    /**
     * @return void
     * @access public
     */
    function getFinalUrl()
    {
        return $this->_url;
    }

    /**
     * @param  object   $subject
     * @param  string   $event
     * @param  mixed    $data
     * @return void
     * @access public
     */
    function update(&$subject, $event, $data = null)
    {
        switch ($event) {
        case 'httpSuccess':
        case 'httpError':
            if ('' == $this->_redirUrl) {
                $this->_url = $this->_checkedUrl;
            } else {
                $this->_url = $this->_redirUrl;
            }
            break;
        case 'httpRedirect':
            $this->_redirUrl = $data;
            break;
        case 'request':
            $this->_checkedUrl = $data;
            $this->_redirUrl = '';
            break;
        }
    }
}

/**
 * The original version doesn't offer public method/property to get
 * the _cookies property which is supposed to be private.
 */
class HTTP_Client_CookieManager_Extended extends HTTP_Client_CookieManager
{
    /**
     * @return void
     * @access public
     */
    function getCookies()
    {
        return $this->_cookies;
    }
}

/**
 * The original version _getMetaRedirect() megthod doesn't suffice
 * for handling various patterns of META refresh. So I override it.
 *
 * Also the original version has no way to handle guid=ON if redirect
 * URL has or has not guid=ON.
 */
class HTTP_Client_Extended extends HTTP_Client
{
   /**
    * @param    string   URL
    * @param    string   Method, constants are defined in HTTP_Request
    * @param    array    Extra headers to send
    * @access   private
    * @return   HTTP_Request    Request object with all defaults applied
    */
    function &_createRequest($url, $method = HTTP_REQUEST_METHOD_GET, $headers = array())
    {
        $req =& new HTTP_Request($url, $this->_defaultRequestParams);
        $req->setMethod($method);
        foreach ($this->_defaultHeaders as $name => $value) {
            $req->addHeader($name, $value);
        }
        foreach ($headers as $name => $value) {
            $req->addHeader($name, $value);
        }
        $this->_cookieManager->passCookies($req);
        foreach ($this->_propagate as $id => $propagate) {
            if ($propagate) {
                $req->attach($this->_listeners[$id]);
            }
        }

        /**
         * Add/remove X-dcmguid header according to the request.
         * This is necessary if a redirected URL has or has not guid=ON.
         */
        $user_agent = $this->getHeader($req, 'user-agent');
        $x_dcmguid = $this->getHeader($req, 'x-dcmguid');
        if (isset($x_dcmguid)
            and isset($user_agent)
            and 0 === strpos($user_agent, 'DoCoMo/')) {
            if (0 === strpos($url, 'https')
                or !MobileProxy::hasGuid($url)) {
                $req->removeHeader('x-dcmguid');
            } else {
                $req->addHeader('x-dcmguid', $x_dcmguid);
            }
        }
        return $req;
    }

   /**
    * @param  resource $request
    * @param  string   $name
    * @return mixed
    * @access private
    */
    function getHeader($request, $name)
    {
        return isset($request->_requestHeaders[$name])?
            $request->_requestHeaders[$name]:
            null;
    }

   /**
    * @param  resource $request
    * @return string
    * @access private
    */
    function _getMetaRedirect(&$request)
    {
        if (!preg_match_all('/<meta\b[^>]*?>/si', $request->getResponseBody(), $matches, PREG_PATTERN_ORDER)) {
            return '';
        }
        $meta_tags = $matches[0];
        $refresh_url = '';
        foreach ($meta_tags as $meta_tag) {
            $search_attrib = '/[\s\'\"]\b%s\b\s*=\s*([^\s\'\">]+|\'[^\']+\'|\"[^\"]+\")/si';
            $search_remove_quote = '/^\s*[\'\"](.+)[\'\"]\s*$/s';
            if (preg_match(sprintf($search_attrib, 'http-equiv'), $meta_tag, $matches)) {
                $value = preg_replace($search_remove_quote, '$1', $matches[1]);
                if (strtolower($value) == 'refresh'
                    and preg_match(sprintf($search_attrib, 'content'), $meta_tag, $matches)) {
                    $value = preg_replace($search_remove_quote, '$1', $matches[1]);
                    /**
                     * Separator can be either ',' or ';'.
                     */
                    if (preg_match('/^\s*\d+\s*[,;]\s*URL\s*=\s*(.+)\s*$/si', $value, $matches)) {
                        $refresh_url = html_entity_decode(trim($matches[1]), ENT_QUOTES);
                        /**
                         * Quoted URL actually works, at least on IE and Gecko.
                         * This pattern can be found in some of Google's page.
                         */
                        $refresh_url = preg_replace('/^[\'"]\s*(.*?)\s*[\'"]$/', '$1', $refresh_url);
                        break;
                    }
                }
            }
        }
        if (empty($refresh_url)) {
            return '';
        }
        /**
         * Though HTTP_Request::_url may be a private property, I dare to use it
         * simply because it is the same way of the original version.
         * HTTP_Client treat it as if it is public, and there seems to be no other way.
         */
        $current_url = $request->_url->getUrl();
        $refresh_url = $this->_redirectUrl($request->_url, $refresh_url);
        /**
         * Do not redirect if the refresh URL is the current URL.
         */
        if (empty($refresh_url) or $refresh_url == $current_url) {
            return '';
        }
        return $refresh_url; 
    }
}
